Skip to content

fix(libkernel): fail dlopen of an unrelinked module without the Windows Bad Image dialog - #9832

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
johandavidcm:fix/libkernel-dlopen-unrelinked
Oct 23, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
johandavidcm:fix/libkernel-dlopen-unrelinked

Conversation

@johandavidcm

@johandavidcm johandavidcm commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What

On Windows, dlopen passes a module that was never relinked (a PS5 ELF or SELF that a title loads with sceKernelLoadStartModule) to LoadLibraryExW. Windows then raises its modal Bad Image hard-error dialog (0xC000012F), and the calling thread blocks until someone answers it. After that, the load fails with error 193 and sceKernelLoadStartModule returns SCE_KERNEL_ERROR_ENOENT, with nothing in the log.

Yakuza 0 Director's Cut (PPSA31327) hits this whenever these modules are not relinked:

  • at every boot, for /app0/data/crm/ps5/crm_module_ps5.prx (the SEGA account module);
  • in chapter 1, for /app0/data/module/ps5/yakuza_minigame_ps5_retail.prx.

Unattended runs stall on the dialog.

  • No dialog: LoadWithoutErrorDialog sets SEM_FAILCRITICALERRORS on the calling thread for the LoadLibraryExW call only, keeps the loader's error code, and restores the previous thread mode. PatchedLibraries.cpp uses the same flag for its whole process. The module's initialization (InitializeDeferredModule) runs after the mode is restored.
  • Same result: the load fails at once with the same error. The title gets null / ENOENT and continues, as it did after someone answered the dialog.
  • Reporting: every failed load writes the dlerror text and prints it on stderr, once per file, with a [module] prefix. This applies on Windows and on the other hosts.
    • A file that exists but has no relinked <name>.guest.prx gets: dlopen: <path> was not relinked: no <name>.guest.prx was found (Windows loader error 193); relink its directory with the relinker's --module-dir <dir> option.
    • Any other failure gets: dlopen: cannot load <path> (<reason>).
  • Docs: USAGE.md mentions the message beside its existing note on error 193.

Overlaps:

Tested

Windows 11 Pro 26200, WinLibs GCC 15.2.0, Release, BUILD_TESTING=ON, on head dc84bec (main a6646f8).

guest_dynamic_loader gains a check. dlopen of an existing 4-byte ELF stub that has no .guest.prx is called twice:

  • with the process error mode reset to the default, because CTest starts tests with critical-error boxes off;
  • with the thread mode set to SEM_NOGPFAULTERRORBOX.

Each call must return null, with a dlerror that names the file and --module-dir, and the thread mode must be unchanged afterwards. The missing-module dlerror must not mention --module-dir.

  • With main's DynamicLoader.cpp, the test shows the Bad Image dialog and fails on the test's new TIMEOUT (30 s, the CI limit).
  • With this change it passes in 0.1 s, prints the was not relinked line once for the two calls, and passes --repeat until-fail:20.

Other tests that pass: guest_dynamic_loader_variant_names, guest_deferred_module_{reload,pinned,parallel,reentrant}, guest_module_info, guest_module_{dir,directories,args,identity,fini} and patched_libraries.

PPSA31327 01.015.000, relinked with only --module-dir data/module/ps5, launched with the default error mode, from boot to gameplay:

  • With libkernel from main 80565c5, the Bad Image dialog for crm_module_ps5.prx appears 6 s after start.
  • With the libraries built from the change on main 0def01b (head 4894d1c, the same diff), no dialog appears, and stderr has one [module] dlopen: ...crm_module_ps5.prx was not relinked ... line. The title reaches gameplay, at about 200,000 draws per 10 s.

Linux: DynamicLoader.cpp and GuestDynamicLoader.cpp compile with GCC 15.2.0 (-Wall -Wextra, no warnings) in WSL Ubuntu. The test was not run there. On Linux and macOS the change is only the reporting: LoadFailure instead of Error(::dlerror()).

Not tested: macOS.

tools/check_conventions.py --base origin/main passes.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: PPSA31327 Yakuza 0 Director's Cut (PPSA31327) #6722
  • GPUs tested: n/a
  • AI-assisted: yes

@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 22, 2026

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed head cc888176de25999eb68d6fc01d0cc641774b58da. python3 tools/check_conventions.py --base upstream/main and git diff --check upstream/main...refs/review/9832 pass. I also ran GCC 12.2 C++20 syntax-only compilation of the changed DynamicLoader.cpp and GuestDynamicLoader.cpp translation units; both compile successfully on Linux.

I did not run the CTest binary or the Windows modal-dialog reproduction here. The PR description reports the focused Windows test, repeated runs, and title reproduction; the PR's hosted Linux/Windows build jobs are skipped. The current main is two commits ahead of this head; a merge-tree check is clean. Please update to current main before merge and ensure the Windows focused test runs on the final head. I found no blocking issue in the reviewed diff.

@JJenkx

JJenkx commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Independent Windows run, as data only. Machine: Windows 11 Enterprise 10.0.22621, Ryzen 9 5950X, WinLibs GCC 15.2.0 UCRT r7, Release, BUILD_TESTING=ON, Python 3.15. #9832, #9793 and #9768 were merged together onto main 11f976b, without conflicts, and each test was run 3 times. guest_dynamic_loader passes 3 of 3.

AI-assisted (Claude Code).

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6586 core/libs/prx/libkernel/Module/src/DynamicLoader.cpp L196
PR 9415 core/libs/prx/libkernel/Module/src/DynamicLoader.cpp L182-184
PR 9488 docs/user/USAGE.md L177

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 4563 core/libs/prx/libkernel/CMakeLists.txt
PR 5021 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Module/src/DynamicLoader.cpp
docs/user/USAGE.md
PR 5925 docs/user/USAGE.md
PR 6044 core/libs/prx/libkernel/CMakeLists.txt
core/libs/tests/GuestDynamicLoader.cpp
PR 6171 docs/user/USAGE.md
PR 6469 core/libs/prx/libkernel/CMakeLists.txt
PR 6544 core/libs/tests/GuestDynamicLoader.cpp
PR 6788 docs/user/USAGE.md
PR 6830 core/libs/prx/libkernel/CMakeLists.txt
PR 6924 docs/user/USAGE.md
PR 6936 core/libs/prx/libkernel/CMakeLists.txt
PR 7201 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Module/src/DynamicLoader.cpp
PR 7642 docs/user/USAGE.md
PR 7800 docs/user/USAGE.md
PR 7918 docs/user/USAGE.md
PR 8520 core/libs/prx/libkernel/CMakeLists.txt
PR 8658 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Module/src/DynamicLoader.cpp
core/libs/tests/GuestDynamicLoader.cpp
PR 9187 core/libs/prx/libkernel/CMakeLists.txt
PR 9284 core/libs/prx/libkernel/CMakeLists.txt
PR 9293 core/libs/prx/libkernel/CMakeLists.txt
PR 9297 core/libs/prx/libkernel/CMakeLists.txt
PR 9302 core/libs/prx/libkernel/CMakeLists.txt
PR 9408 docs/user/USAGE.md
PR 9508 docs/user/USAGE.md
PR 9561 core/libs/prx/libkernel/CMakeLists.txt
PR 9566 core/libs/prx/libkernel/CMakeLists.txt
PR 9575 core/libs/prx/libkernel/CMakeLists.txt
PR 9669 core/libs/prx/libkernel/CMakeLists.txt
PR 9693 core/libs/prx/libkernel/CMakeLists.txt

@johandavidcm
johandavidcm force-pushed the fix/libkernel-dlopen-unrelinked branch from cc88817 to 50a14b1 Compare October 22, 2026 17:21
@johandavidcm

Copy link
Copy Markdown
Contributor Author

Rebased onto main 499b1cf; the head is now 50a14b1. On that head (Windows 11 Pro 26200, WinLibs GCC 15.2.0, Release, BUILD_TESTING=ON), these pass:

  • guest_dynamic_loader, with --repeat until-fail:20.
  • guest_dynamic_loader_variant_names, guest_deferred_module_{reload,pinned,parallel,reentrant}, guest_module_info, guest_module_{dir,directories,args,identity,fini} and patched_libraries.
  • tools/check_conventions.py --base origin/main.

@johandavidcm
johandavidcm force-pushed the fix/libkernel-dlopen-unrelinked branch from cc88817 to 50a14b1 Compare October 22, 2026 17:22
@MarcZX10

Copy link
Copy Markdown
Contributor

Follow-up on the refreshed head 50a14b1577: I compiled the exact PR DynamicLoader.cpp and GuestDynamicLoader.cpp with GCC 12.2 C++20 -Wall -Wextra -Werror -fsyntax-only on Linux; both passed. The PR is mergeable, but its base commit is still 499b1cf9fc, 24 commits behind current main 6a6a35fdf5c2, and the reported Windows test used 751b2881a. CONTRIBUTING requires a current-main base; please rebase and rerun the focused Windows guest_dynamic_loader/title check on that final head before merge. I found no new code blocker.

@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for rebasing and reporting the 20-repeat Windows guest_dynamic_loader run plus the related module tests. I also ran Linux -fsyntax-only -Wall -Wextra -Werror on the exact DynamicLoader.cpp and GuestDynamicLoader.cpp; both pass. Since that update, current main has advanced again: the head is now 52 commits behind the current snapshot, and this diff overlaps the open fallback/macos loader work noted by the overlap report. Please refresh onto current main and rerun the focused Windows test on the final combined head before approval. This is a contribution-rule/reviewability request, not a merge request.

…ws Bad Image dialog

On Windows, dlopen passed a module that was never relinked to LoadLibraryExW, which raised the
modal Bad Image hard-error dialog and blocked the calling thread until someone answered it. The
load now runs with SEM_FAILCRITICALERRORS set for that thread only, so it fails at once with the
same result as before. Every failed load is reported once per file on stderr with a [module]
prefix; a file that exists with no relinked .guest.prx names the relinker's --module-dir option.
@johandavidcm
johandavidcm force-pushed the fix/libkernel-dlopen-unrelinked branch from 4894d1c to dc84bec Compare October 22, 2026 18:21
@johandavidcm

Copy link
Copy Markdown
Contributor Author

The previous push (4894d1c) never reached this PR, which still showed 50a14b1, during today's GitHub incident with pull requests. Rebased again, onto main a6646f8; the head is now dc84bec, 0 commits behind. On that head (Windows 11 Pro 26200, WinLibs GCC 15.2.0, Release, BUILD_TESTING=ON):

  • guest_dynamic_loader passes --repeat until-fail:20.
  • guest_dynamic_loader_variant_names, guest_deferred_module_{reload,pinned,parallel,reentrant}, guest_module_info, guest_module_{dir,directories,args,identity,fini} and patched_libraries pass.
  • tools/check_conventions.py --base origin/main passes.

On 4894d1c, which is the same diff on main 0def01b:

  • With that main's DynamicLoader.cpp, the test showed the Bad Image dialog and failed on its 30 s TIMEOUT.
  • PPSA31327, launched with the default error mode, showed no dialog, logged one [module] ... crm_module_ps5.prx was not relinked ... line and reached gameplay.

The Tested section of the description names this head.

On the overlaps with the loader work, #9415 (fallback paths) and #8658/#6586 (macOS and Linux POSIX branch), this PR depends on none of them; each pair only conflicts on lines. I asked about the order in #9415.

AI-assisted: yes

@johandavidcm
johandavidcm force-pushed the fix/libkernel-dlopen-unrelinked branch from 50a14b1 to 4894d1c Compare October 22, 2026 18:22
@oneandonlydean oneandonlydean self-assigned this Oct 22, 2026
@oneandonlydean
oneandonlydean merged commit 5e9f4b8 into boykopovar:main Oct 23, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build docs libkernel unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants